"""
Anomaly detection utilities for time series metrics.
"""
import pandas as pd
import numpy as np
from typing import List, Optional, Tuple
from datetime import datetime
from scipy import stats
from ..models.schemas import AnomalyResult, SeverityLevel


class AnomalyDetector:
    """Detects anomalies in time series data."""

    def __init__(self, threshold: float = 2.0):
        """
        Initialize anomaly detector.

        Args:
            threshold: Number of standard deviations for anomaly threshold
        """
        self.threshold = threshold

    def detect_anomalies(
        self,
        df: pd.DataFrame,
        metric_column: str,
        time_column: str = 'date',
        method: str = 'zscore'
    ) -> List[AnomalyResult]:
        """
        Detect anomalies in a time series.

        Args:
            df: DataFrame with time series data
            metric_column: Column name of the metric to analyze
            time_column: Column name for timestamps
            method: Detection method ('zscore', 'iqr', 'moving_avg')

        Returns:
            List of AnomalyResult objects
        """
        if method == 'zscore':
            return self._detect_zscore(df, metric_column, time_column)
        elif method == 'iqr':
            return self._detect_iqr(df, metric_column, time_column)
        elif method == 'moving_avg':
            return self._detect_moving_avg(df, metric_column, time_column)
        else:
            raise ValueError(f"Unknown detection method: {method}")

    def _detect_zscore(
        self,
        df: pd.DataFrame,
        metric_column: str,
        time_column: str
    ) -> List[AnomalyResult]:
        """Detect anomalies using Z-score method."""
        anomalies = []

        values = df[metric_column].values
        mean = np.mean(values)
        std = np.std(values)

        if std == 0:
            return []

        z_scores = np.abs((values - mean) / std)

        for idx, z_score in enumerate(z_scores):
            if z_score > self.threshold:
                actual_value = float(values[idx])
                deviation = actual_value - mean

                anomalies.append(AnomalyResult(
                    metric=metric_column,
                    timestamp=df.iloc[idx][time_column],
                    actual_value=actual_value,
                    expected_value=mean,
                    deviation=deviation,
                    deviation_percentage=abs(deviation / mean * 100) if mean != 0 else 0,
                    severity=self._calculate_severity(z_score),
                    confidence=min(z_score / (self.threshold * 2), 1.0),
                    method='zscore'
                ))

        return anomalies

    def _detect_iqr(
        self,
        df: pd.DataFrame,
        metric_column: str,
        time_column: str
    ) -> List[AnomalyResult]:
        """Detect anomalies using Interquartile Range (IQR) method."""
        anomalies = []

        values = df[metric_column].values
        q1 = np.percentile(values, 25)
        q3 = np.percentile(values, 75)
        iqr = q3 - q1

        lower_bound = q1 - 1.5 * iqr
        upper_bound = q3 + 1.5 * iqr

        median = np.median(values)

        for idx, value in enumerate(values):
            if value < lower_bound or value > upper_bound:
                actual_value = float(value)
                deviation = actual_value - median

                anomalies.append(AnomalyResult(
                    metric=metric_column,
                    timestamp=df.iloc[idx][time_column],
                    actual_value=actual_value,
                    expected_value=median,
                    deviation=deviation,
                    deviation_percentage=abs(deviation / median * 100) if median != 0 else 0,
                    severity=self._calculate_severity_iqr(value, lower_bound, upper_bound, iqr),
                    confidence=0.8,
                    method='iqr'
                ))

        return anomalies

    def _detect_moving_avg(
        self,
        df: pd.DataFrame,
        metric_column: str,
        time_column: str,
        window: int = 7
    ) -> List[AnomalyResult]:
        """Detect anomalies using moving average method."""
        anomalies = []

        df_sorted = df.sort_values(time_column).copy()
        df_sorted['ma'] = df_sorted[metric_column].rolling(window=window, min_periods=1).mean()
        df_sorted['std'] = df_sorted[metric_column].rolling(window=window, min_periods=1).std()

        for idx in range(len(df_sorted)):
            actual = float(df_sorted.iloc[idx][metric_column])
            expected = float(df_sorted.iloc[idx]['ma'])
            std = float(df_sorted.iloc[idx]['std'])

            if std > 0:
                z_score = abs((actual - expected) / std)

                if z_score > self.threshold:
                    deviation = actual - expected

                    anomalies.append(AnomalyResult(
                        metric=metric_column,
                        timestamp=df_sorted.iloc[idx][time_column],
                        actual_value=actual,
                        expected_value=expected,
                        deviation=deviation,
                        deviation_percentage=abs(deviation / expected * 100) if expected != 0 else 0,
                        severity=self._calculate_severity(z_score),
                        confidence=min(z_score / (self.threshold * 2), 1.0),
                        method='moving_avg'
                    ))

        return anomalies

    def _calculate_severity(self, z_score: float) -> SeverityLevel:
        """Calculate severity level based on Z-score."""
        if z_score > 4:
            return SeverityLevel.CRITICAL
        elif z_score > 3:
            return SeverityLevel.HIGH
        elif z_score > 2:
            return SeverityLevel.MEDIUM
        else:
            return SeverityLevel.LOW

    def _calculate_severity_iqr(
        self,
        value: float,
        lower_bound: float,
        upper_bound: float,
        iqr: float
    ) -> SeverityLevel:
        """Calculate severity level for IQR method."""
        if value < lower_bound:
            distance = (lower_bound - value) / iqr
        else:
            distance = (value - upper_bound) / iqr

        if distance > 3:
            return SeverityLevel.CRITICAL
        elif distance > 2:
            return SeverityLevel.HIGH
        elif distance > 1.5:
            return SeverityLevel.MEDIUM
        else:
            return SeverityLevel.LOW

    def find_most_significant_anomaly(
        self,
        anomalies: List[AnomalyResult]
    ) -> Optional[AnomalyResult]:
        """Find the most significant anomaly from a list."""
        if not anomalies:
            return None

        # Sort by severity and then by deviation percentage
        severity_order = {
            SeverityLevel.CRITICAL: 4,
            SeverityLevel.HIGH: 3,
            SeverityLevel.MEDIUM: 2,
            SeverityLevel.LOW: 1
        }

        return max(
            anomalies,
            key=lambda a: (severity_order[a.severity], abs(a.deviation_percentage))
        )

    def aggregate_anomalies_by_dimension(
        self,
        df: pd.DataFrame,
        metric_column: str,
        dimension_column: str,
        time_column: str = 'date'
    ) -> dict:
        """
        Detect anomalies for each dimension value separately.

        Returns:
            Dictionary mapping dimension values to their anomalies
        """
        result = {}

        for dimension_value in df[dimension_column].unique():
            subset = df[df[dimension_column] == dimension_value]
            if len(subset) > 3:  # Need minimum data points
                anomalies = self.detect_anomalies(subset, metric_column, time_column)
                if anomalies:
                    result[dimension_value] = anomalies

        return result
